/*
 * Copyright (c) 2013 Yaroslav Stavnichiy <yarosla@gmail.com>
 *
 * This file is part of NXJSON.
 *
 * NXJSON is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *
 * NXJSON is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with NXJSON. If not, see <http://www.gnu.org/licenses/>.
 */

// this file can be #included in your code
#ifndef NXJSON_C
#define NXJSON_C

#ifdef __cplusplus
extern "C" {
#endif

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <furi.h>

#include "nxjson.h"

// redefine NX_JSON_CALLOC & NX_JSON_FREE to use custom allocator
#ifndef NX_JSON_CALLOC
#define NX_JSON_CALLOC()   calloc(1, sizeof(nx_json))
#define NX_JSON_FREE(json) free((void*)(json))
#endif

// redefine NX_JSON_REPORT_ERROR to use custom error reporting
#ifndef NX_JSON_REPORT_ERROR
#define NX_JSON_REPORT_ERROR(msg, p) FURI_LOG_E("nxjson", "PARSE ERROR (%d): at %s", __LINE__, p)
#endif

#define IS_WHITESPACE(c) ((unsigned char)(c) <= (unsigned char)' ')

static nx_json* create_json(nx_json_type type, const char* key, nx_json* parent) {
    nx_json* js = NX_JSON_CALLOC();
    assert(js);
    js->type = type;
    js->key = key;
    if(!parent->children.last) {
        parent->children.first = parent->children.last = js;
    } else {
        parent->children.last->next = js;
        parent->children.last = js;
    }
    parent->children.length++;
    return js;
}

void nx_json_free(const nx_json* js) {
    if(!js) {
        return;
    }
    if(js->type == NX_JSON_OBJECT || js->type == NX_JSON_ARRAY) {
        nx_json* p = js->children.first;
        nx_json* p1;
        while(p) {
            p1 = p->next;
            nx_json_free(p);
            p = p1;
        }
    }
    NX_JSON_FREE(js);
}

static int unicode_to_utf8(unsigned int codepoint, char* p, char** endp) {
    // code from http://stackoverflow.com/a/4609989/697313
    if(codepoint < 0x80)
        *p++ = codepoint;
    else if(codepoint < 0x800)
        *p++ = 192 + codepoint / 64, *p++ = 128 + codepoint % 64;
    else if(codepoint - 0xd800u < 0x800)
        return 0; // surrogate must have been treated earlier
    else if(codepoint < 0x10000)
        *p++ = 224 + codepoint / 4096, *p++ = 128 + codepoint / 64 % 64,
        *p++ = 128 + codepoint % 64;
    else if(codepoint < 0x110000)
        *p++ = 240 + codepoint / 262144, *p++ = 128 + codepoint / 4096 % 64,
        *p++ = 128 + codepoint / 64 % 64, *p++ = 128 + codepoint % 64;
    else
        return 0; // error
    *endp = p;
    return 1;
}

nx_json_unicode_encoder nx_json_unicode_to_utf8 = unicode_to_utf8;

static inline int hex_val(char c) {
    if(c >= '0' && c <= '9') return c - '0';
    if(c >= 'a' && c <= 'f') return c - 'a' + 10;
    if(c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

static char* unescape_string(char* s, char** end, nx_json_unicode_encoder encoder) {
    char* p = s;
    char* d = s;
    char c;
    while((c = *p++)) {
        if(c == '"') {
            *d = '\0';
            *end = p;
            return s;
        } else if(c == '\\') {
            switch(*p) {
            case '\\':
            case '/':
            case '"':
                *d++ = *p++;
                break;
            case 'b':
                *d++ = '\b';
                p++;
                break;
            case 'f':
                *d++ = '\f';
                p++;
                break;
            case 'n':
                *d++ = '\n';
                p++;
                break;
            case 'r':
                *d++ = '\r';
                p++;
                break;
            case 't':
                *d++ = '\t';
                p++;
                break;
            case 'u': // unicode
                if(!encoder) {
                    // leave untouched
                    *d++ = c;
                    break;
                }
                char* ps = p - 1;
                int h1, h2, h3, h4;
                if((h1 = hex_val(p[1])) < 0 || (h2 = hex_val(p[2])) < 0 ||
                   (h3 = hex_val(p[3])) < 0 || (h4 = hex_val(p[4])) < 0) {
                    NX_JSON_REPORT_ERROR("invalid unicode escape", p - 1);
                    return 0;
                }
                unsigned int codepoint = h1 << 12 | h2 << 8 | h3 << 4 | h4;
                if((codepoint & 0xfc00) ==
                   0xd800) { // high surrogate; need one more unicode to succeed
                    p += 6;
                    if(p[-1] != '\\' || *p != 'u' || (h1 = hex_val(p[1])) < 0 ||
                       (h2 = hex_val(p[2])) < 0 || (h3 = hex_val(p[3])) < 0 ||
                       (h4 = hex_val(p[4])) < 0) {
                        NX_JSON_REPORT_ERROR("invalid unicode surrogate", ps);
                        return 0;
                    }
                    unsigned int codepoint2 = h1 << 12 | h2 << 8 | h3 << 4 | h4;
                    if((codepoint2 & 0xfc00) != 0xdc00) {
                        NX_JSON_REPORT_ERROR("invalid unicode surrogate", ps);
                        return 0;
                    }
                    codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (codepoint2 - 0xdc00);
                }
                if(!encoder(codepoint, d, &d)) {
                    NX_JSON_REPORT_ERROR("invalid codepoint", ps);
                    return 0;
                }
                p += 5;
                break;
            default:
                // leave untouched
                *d++ = c;
                break;
            }
        } else {
            *d++ = c;
        }
    }
    NX_JSON_REPORT_ERROR("no closing quote for string", s);
    return 0;
}

static char* skip_block_comment(char* p) {
    // assume p[-2]=='/' && p[-1]=='*'
    char* ps = p - 2;
    if(!*p) {
        NX_JSON_REPORT_ERROR("endless comment", ps);
        return 0;
    }
REPEAT:
    p = strchr(p + 1, '/');
    if(!p) {
        NX_JSON_REPORT_ERROR("endless comment", ps);
        return 0;
    }
    if(p[-1] != '*') goto REPEAT;
    return p + 1;
}

static char* parse_key(const char** key, char* p, nx_json_unicode_encoder encoder) {
    // on '}' return with *p=='}'
    char c;
    while((c = *p++)) {
        if(c == '"') {
            *key = unescape_string(p, &p, encoder);
            if(!*key) return 0; // propagate error
            while(*p && IS_WHITESPACE(*p))
                p++;
            if(*p == ':') return p + 1;
            NX_JSON_REPORT_ERROR("unexpected chars", p);
            return 0;
        } else if(IS_WHITESPACE(c) || c == ',') {
            // continue
        } else if(c == '}') {
            return p - 1;
        } else if(c == '/') {
            if(*p == '/') { // line comment
                char* ps = p - 1;
                p = strchr(p + 1, '\n');
                if(!p) {
                    NX_JSON_REPORT_ERROR("endless comment", ps);
                    return 0; // error
                }
                p++;
            } else if(*p == '*') { // block comment
                p = skip_block_comment(p + 1);
                if(!p) return 0;
            } else {
                NX_JSON_REPORT_ERROR("unexpected chars", p - 1);
                return 0; // error
            }
        } else {
            NX_JSON_REPORT_ERROR("unexpected chars", p - 1);
            return 0; // error
        }
    }
    NX_JSON_REPORT_ERROR("unexpected chars", p - 1);
    return 0; // error
}

static char*
    parse_value(nx_json* parent, const char* key, char* p, nx_json_unicode_encoder encoder) {
    nx_json* js;
    while(1) {
        switch(*p) {
        case '\0':
            NX_JSON_REPORT_ERROR("unexpected end of text", p);
            return 0; // error
        case ' ':
        case '\t':
        case '\n':
        case '\r':
        case ',':
            // skip
            p++;
            break;
        case '{':
            js = create_json(NX_JSON_OBJECT, key, parent);
            p++;
            while(1) {
                const char* new_key = NULL;
                p = parse_key(&new_key, p, encoder);
                if(!p) return 0; // error
                if(*p == '}') return p + 1; // end of object
                p = parse_value(js, new_key, p, encoder);
                if(!p) return 0; // error
            }
        case '[':
            js = create_json(NX_JSON_ARRAY, key, parent);
            p++;
            while(1) {
                p = parse_value(js, 0, p, encoder);
                if(!p) return 0; // error
                if(*p == ']') return p + 1; // end of array
            }
        case ']':
            return p;
        case '"':
            p++;
            js = create_json(NX_JSON_STRING, key, parent);
            js->text_value = unescape_string(p, &p, encoder);
            if(!js->text_value) return 0; // propagate error
            return p;
        case '-':
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9': {
            js = create_json(NX_JSON_INTEGER, key, parent);
            char* pe;
            if(*p == '-') {
                js->num.s_value = (nxjson_s64)strtol(p, &pe, 0); // was strtoll
            } else {
                js->num.u_value = (nxjson_u64)strtoul(p, &pe, 0); // was stroull
            }
            if(pe == p || errno == ERANGE) {
                NX_JSON_REPORT_ERROR("invalid number", p);
                return 0; // error
            }
            if(*pe == '.' || *pe == 'e' || *pe == 'E') { // float value
                js->type = NX_JSON_float;
                js->num.dbl_value = strtod(p, &pe);
                if(pe == p || errno == ERANGE) {
                    NX_JSON_REPORT_ERROR("invalid number", p);
                    return 0; // error
                }
            } else {
                if(*p == '-') {
                    js->num.dbl_value = js->num.s_value;
                } else {
                    js->num.dbl_value = js->num.u_value;
                }
            }
            return pe;
        }
        case 't':
            if(!strncmp(p, "true", 4)) {
                js = create_json(NX_JSON_BOOL, key, parent);
                js->num.u_value = 1;
                return p + 4;
            }
            NX_JSON_REPORT_ERROR("unexpected chars", p);
            return 0; // error
        case 'f':
            if(!strncmp(p, "false", 5)) {
                js = create_json(NX_JSON_BOOL, key, parent);
                js->num.u_value = 0;
                return p + 5;
            }
            NX_JSON_REPORT_ERROR("unexpected chars", p);
            return 0; // error
        case 'n':
            if(!strncmp(p, "null", 4)) {
                create_json(NX_JSON_NULL, key, parent);
                return p + 4;
            }
            NX_JSON_REPORT_ERROR("unexpected chars", p);
            return 0; // error
        case '/': // comment
            if(p[1] == '/') { // line comment
                char* ps = p;
                p = strchr(p + 2, '\n');
                if(!p) {
                    NX_JSON_REPORT_ERROR("endless comment", ps);
                    return 0; // error
                }
                p++;
            } else if(p[1] == '*') { // block comment
                p = skip_block_comment(p + 2);
                if(!p) return 0;
            } else {
                NX_JSON_REPORT_ERROR("unexpected chars", p);
                return 0; // error
            }
            break;
        default:
            NX_JSON_REPORT_ERROR("unexpected chars", p);
            return 0; // error
        }
    }
}

const nx_json* nx_json_parse_utf8(char* text) {
    return nx_json_parse(text, unicode_to_utf8);
}

const nx_json* nx_json_parse(char* text, nx_json_unicode_encoder encoder) {
    nx_json js = {0};
    if(!parse_value(&js, 0, text, encoder)) {
        if(js.children.first) nx_json_free(js.children.first);
        return 0;
    }
    return js.children.first;
}

const nx_json* nx_json_get(const nx_json* json, const char* key) {
    nx_json* js;
    for(js = json->children.first; js; js = js->next) {
        if(js->key && !strcmp(js->key, key)) return js;
    }
    return NULL;
}

const nx_json* nx_json_item(const nx_json* json, int idx) {
    nx_json* js;
    for(js = json->children.first; js; js = js->next) {
        if(!idx--) return js;
    }
    return NULL;
}

#ifdef __cplusplus
}
#endif

#endif /* NXJSON_C */
